﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;

using OpGen.Extensions.Runtime.Server;

namespace OpGen.Extensions.Network
{
    /// <summary>
    /// Stellt die Socketanbindung des Mainframe-Servers bereit
    /// </summary>
    /// <typeparam name="TClient">Eine Klasse die ein Client representiert und sich von ServerClientSocket ableiten</typeparam>
    public abstract class ServerSocket<TClient> : ServerMessageMethodInvoker<TClient> where TClient : ServerClientSocket
    {
        private IPEndPoint _localIPEndPoint;
        private Socket _socket;

        private Dictionary<Guid, TClient> _clients ;

        #region Konstruktor

        /// <summary>
        /// Erstellt eine neue Instanz der ServerSocket-Klasse
        /// </summary>
        protected ServerSocket()
        {
            _clients = new Dictionary<Guid, TClient>();
        }

        /// <summary>
        /// Erstellt eine neue Instanz der ServerSocket-Klasse
        /// </summary>
        /// <param name="useIPv6">Gibt an ob IPv6 genutzt werden kann</param>
        /// <param name="port">Gibt den Port an auf den eingehende Verbindungsanfragen gerichtet werden</param>
        public ServerSocket(bool useIPv6, int port)
        {
            _clients = new Dictionary<Guid, TClient>();

            if (useIPv6)
                CreateLocalEnpoint(IPAddress.IPv6Any, port);
            else
                CreateLocalEnpoint(IPAddress.Any, port);
        }

        /// <summary>
        /// Erstellt eine neue Instanz der ServerSocket-Klasse
        /// </summary>
        /// <param name="address">Gibt Adresse an auf den die eingehenden Verbindungsanfragen gerichtet werden</param>
        /// <param name="port">Gibt den Port an auf den eingehende Verbindungsanfragen gerichtet werden</param>
        public ServerSocket(IPAddress address, int port)
        {
            _clients = new Dictionary<Guid, TClient>();

            CreateLocalEnpoint(address, port);
        }

        /// <summary>
        /// Erstellt einen Lokalen IP Endpoint für eingehende verbindungen
        /// </summary>
        /// <param name="address"></param>
        /// <param name="port"></param>
        protected void CreateLocalEnpoint(IPAddress address, int port)
        {
            _localIPEndPoint = new IPEndPoint(address, port);
        }

        #endregion

        /// <summary>
        /// Startet des Serverbetrieb und erstellt ein Socket für eingehende Verbindungen
        /// </summary>
        public virtual void Start()
        {
            if (IsRunning)
                return;

            try
            {
                _socket = new Socket(_localIPEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            }
            catch (Exception ex)
            {
                throw new OpGenException("Faild to create Socket instance", ex);
            }

            try
            {
                _socket.Bind(_localIPEndPoint);
            }
            catch (Exception ex)
            {
                throw new OpGenException("Faild to bind socket on" + _localIPEndPoint.ToString(), ex);
            }

            try
            {
                _socket.Listen(200);
                _socket.BeginAccept(Internal_AcceptCallback, null);
            }
            catch (Exception ex)
            {
                throw new OpGenException("Faild to socket setup listen mode", ex);
            }
        }

        /// <summary>
        /// Stoppt den Server und entfernt das Socket für eingehende Verbindungen
        /// 
        /// Client Verbindungen bleiben bestehen
        /// </summary>
        public virtual void Stop()
        {
            if (!IsRunning)
                return;

            _socket.Close();
            _socket = null;
        }

        /// <summary>
        /// Stoppt den Server unf trennt alle Client Verbindungen
        /// </summary>
        public virtual void Shutdown()
        {
            Stop();

            if (IsRunning) return;

            foreach (var client in _clients)
            {
                client.Value.Close();
            }
            
        }

        private void Internal_AcceptCallback(IAsyncResult ar)
        {
            if (!IsRunning) return;

            try
            {
                var socket = _socket.EndAccept(ar);
                var client = (TClient)Activator.CreateInstance(typeof(TClient), socket);

                client.ClientDisconnect += new DisconnectDelegate(Internal_ClientDisconnect);
                client.ClientReceive += new ReceiveDelegate(Internal_ClientReceive);

                OnClientConnect(client);

            }
            catch (Exception ex)
            {
                throw new OpGenException("Faild to accept client socket", ex);
            }
            finally
            {
                if (IsRunning)
                    _socket.BeginAccept(Internal_AcceptCallback, null);
            }
        }

        private void Internal_ClientReceive(IConnection client, byte[] data)
        {
            OnClientReceive((TClient)client, data);
        }

        private void Internal_ClientDisconnect(IConnection client)
        {
            OnClientDisconnect((TClient)client);
        }

        /// <summary>
        /// Wird aufgerufen wenn eine neue Verbindung eingerichtet wurde
        /// </summary>
        /// <param name="client">Der Client</param>
        protected virtual void OnClientConnect(TClient client)
        {
            _clients.Add(client.ID, client);
        }

        /// <summary>
        /// Wird aufgerufen wenn eine Verbindung getrennt wird
        /// </summary>
        /// <param name="client"></param>
        protected virtual void OnClientDisconnect(TClient client)
        {
            _clients.Remove(client.ID);
        }

        /// <summary>
        /// Wird aufgerufen wenn Daten vom Client empfangen wurden
        /// </summary>
        /// <param name="client"></param>
        /// <param name="data"></param>
        protected virtual void OnClientReceive(TClient client, byte[] data)
        {
            Invoke(client, data);
        }

        /// <summary>
        /// Gibt an ob der Server aktiv ist
        /// </summary>
        public bool IsRunning
        {
            get { return _socket != null; }
        }
    }
}
